﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Net.Sockets;

using TraitorAmongUs.Exceptions;

namespace TraitorAmongUs.Network {
	public class Packet : IDisposable {
		public static int MAX_PACKET_SIZE = 204800;
		public static int PACKET_HEADER = 0x00AABBCC;
		public static int PACKET_HEADER_SIZE =
			sizeof(int) +	// Header Signature
			sizeof(int) +	// Packet Type
			sizeof(int);	// Body Length

		/*
		 * Packet Types are prefixed with SERVER_ or CLIENT_ depending
		 * on who RECEIEVES the packet
		 */
		public enum PacketTypes {
			CLIENT_VERSIONPACKET = 0x00000001,
			CLIENT_SERVERFULL,

			CLIENT_PACKETSTART = 0x00000010,
			CLIENT_LOBBY_CHATMESSAGE, /* string username, string message */
			CLIENT_LOBBY_START_GAME,

			CLIENT_GAME_LOADED,
			CLIENT_GAME_INIT_DATA,	/* [ int x, int y, int state, string spritePath ] x 10 */
			CLIENT_GAME_UPDATE_DATA, /* [ int x, int y, int state ] x 10 */
			CLIENT_GAME_UPDATE_CROSSHAIR, /* int count, [ int x, int y, int r, int g, int b, int bulletCount ] */
			CLIENT_GAME_UPDATE_BULLETCOUNT, /* int bulletCount */
			CLIENT_GAME_WINNER, /* string username */

			SERVER_PACKETSTART = 0x10000010,
			SERVER_LOBBY_CHATMESSSAGE, /* string message */
			SERVER_LOBBY_SET_HOST,
			SERVER_LOBBY_START_GAME,

			SERVER_GAME_LOADED,
			SERVER_GAME_INIT_DATA_RECEIVED,
			SERVER_GAME_UPDATE_STATE, /* int state */
			SERVER_GAME_UPDATE_CROSSHAIR, /* int x, int y */
			SERVER_GAME_UPDATE_CROSSHAIR_COLOR, /* int r, int g, int b */
			SERVER_GAME_SHOOT, /* int x, int y */

			MAX_PACKET = 0x7FFFFFFF
		};

		MemoryStream packetStream;

		public byte[] Bytes {
			get {
				if(packetStream != null) {
					return packetStream.ToArray();
				} else {
					return null;
				}
			}
		}

		// Used when reading the packet from a stream
		public PacketTypes Type;

		public Packet() {
			packetStream = new MemoryStream();
		}

		public Packet(byte[] bytes) {
			packetStream = new MemoryStream(bytes);
		}

		public void Dispose() {
			if(packetStream != null) {
				packetStream.Dispose();
				packetStream = null;
			}
		}

		public void Send(NetworkStream networkStream, PacketTypes packetType) {
			byte[] buffer = packetStream.ToArray();

			// Add the header
			using(MemoryStream fullPacketStream = new MemoryStream()) {
				WriteHeader((int)packetType, fullPacketStream);
				byte[] packet = fullPacketStream.ToArray();

				networkStream.Write(packet, 0, packet.Length);
			}
		}

		#region Read/Write Methods
		#region Write Methods
		public void Write<T>(T val, MemoryStream stream = null) {
			if(stream == null) stream = packetStream;

			dynamic dval = val;
			byte[] bytes = BitConverter.GetBytes(dval);
			if(BitConverter.IsLittleEndian) Array.Reverse(bytes);

			stream.Write(bytes, 0, bytes.Length);
		}

		public void Write(byte val, MemoryStream stream = null) {
			if(stream == null) stream = packetStream;

			stream.WriteByte(val);
		}

		public void Write(string val, MemoryStream stream = null) {
			if(stream == null) stream = packetStream;

			byte[] bytes = Encoding.ASCII.GetBytes(val);
			Write((short)bytes.Length);
			stream.Write(bytes, 0, bytes.Length);
		}

		public void WriteHeader(int packetType, MemoryStream fullPacketStream) {
			byte[] packetBody = packetStream.ToArray();

			Write(PACKET_HEADER, fullPacketStream);
			Write(packetType, fullPacketStream);
			if(packetBody != null) {
				Write(packetBody.Length, fullPacketStream); // Body Length
				fullPacketStream.Write(packetBody, 0, packetBody.Length);
			} else {
				Write<int>(0, fullPacketStream);	// No body
			}
		}
		#endregion // Write Methods

		#region Read Methods
		public byte ReadByte(MemoryStream stream = null, bool throwException = false) {
			if(stream == null) stream = packetStream;

			int byt = stream.ReadByte();

			if(throwException) {
				if(byt < 0) throw new EndOfStreamException();
			}

			return (byte)byt;
		}

		public int ReadInt(MemoryStream stream = null) {
			if(stream == null) stream = packetStream;

			byte[] bytes = new byte[sizeof(int)];
			stream.Read(bytes, 0, sizeof(int));

			if(BitConverter.IsLittleEndian) Array.Reverse(bytes);

			return BitConverter.ToInt32(bytes, 0);
		}

		public uint ReadUInt(MemoryStream stream = null) {
			if(stream == null) stream = packetStream;

			byte[] bytes = new byte[sizeof(uint)];
			stream.Read(bytes, 0, sizeof(uint));

			if(BitConverter.IsLittleEndian) Array.Reverse(bytes);

			return BitConverter.ToUInt32(bytes, 0);
		}

		public short ReadShort(MemoryStream stream = null) {
			if(stream == null) stream = packetStream;

			byte[] bytes = new byte[sizeof(short)];
			stream.Read(bytes, 0, sizeof(short));

			if(BitConverter.IsLittleEndian) Array.Reverse(bytes);

			return BitConverter.ToInt16(bytes, 0);
		}

		public ushort ReadUShort(MemoryStream stream = null) {
			if(stream == null) stream = packetStream;

			byte[] bytes = new byte[sizeof(ushort)];
			stream.Read(bytes, 0, sizeof(ushort));

			if(BitConverter.IsLittleEndian) Array.Reverse(bytes);

			return BitConverter.ToUInt16(bytes, 0);
		}

		public long ReadLong(MemoryStream stream = null) {
			if(stream == null) stream = packetStream;

			byte[] bytes = new byte[sizeof(long)];
			stream.Read(bytes, 0, sizeof(long));

			if(BitConverter.IsLittleEndian) Array.Reverse(bytes);

			return BitConverter.ToInt64(bytes, 0);
		}

		public ulong ReadULong(MemoryStream stream = null) {
			if(stream == null) stream = packetStream;

			byte[] bytes = new byte[sizeof(ulong)];
			stream.Read(bytes, 0, sizeof(ulong));

			if(BitConverter.IsLittleEndian) Array.Reverse(bytes);

			return BitConverter.ToUInt64(bytes, 0);
		}

		public double ReadDouble(MemoryStream stream = null) {
			if(stream == null) stream = packetStream;

			byte[] bytes = new byte[sizeof(double)];
			stream.Read(bytes, 0, sizeof(double));

			if(BitConverter.IsLittleEndian) Array.Reverse(bytes);

			return BitConverter.ToDouble(bytes, 0);
		}

		public ushort ReadChar(MemoryStream stream = null) {
			if(stream == null) stream = packetStream;

			byte[] bytes = new byte[sizeof(char)];
			stream.Read(bytes, 0, sizeof(char));

			if(BitConverter.IsLittleEndian) Array.Reverse(bytes);

			return BitConverter.ToChar(bytes, 0);
		}

		public bool ReadBoolean(MemoryStream stream = null) {
			if(stream == null) stream = packetStream;

			byte[] bytes = new byte[sizeof(bool)];
			stream.Read(bytes, 0, sizeof(bool));

			if(BitConverter.IsLittleEndian) Array.Reverse(bytes);

			return BitConverter.ToBoolean(bytes, 0);
		}

		public string ReadString(MemoryStream stream = null) {
			if(stream == null) stream = packetStream;

			short length = ReadShort();

			byte[] bytes = new byte[length];
			stream.Read(bytes, 0, length);

			return Encoding.ASCII.GetString(bytes);
		}

		/// <summary>
		/// Reads a packet from the network stream
		/// </summary>
		/// <param name="clientStream">The network stream to read from</param>
		/// <exception cref="ConnectionInterruptedException">Thrown when the connection was interrupted</exception>
		/// <exception cref="HeaderMismatchException">Thrown when an incorrect header signature was received</exception>
		public void ReadPacket(NetworkStream clientStream) {
			byte[] packetHeader = new byte[PACKET_HEADER_SIZE];
			int bytesRead = 0;
			int bytesReadThisPass = 0;

			// Read the header
			do {
				try {
					bytesReadThisPass = clientStream.Read(packetHeader, bytesRead, packetHeader.Length);
				} catch(Exception e) {
					throw e;
				}

				bytesRead += bytesReadThisPass;
			} while(bytesRead < packetHeader.Length && bytesReadThisPass != 0);

			if(bytesReadThisPass == 0) throw new ConnectionInterruptedException();

			using(MemoryStream headerStream = new MemoryStream(packetHeader)) {
				int header = ReadInt(headerStream);
				if(header != PACKET_HEADER) {
					throw new HeaderMismatchException(header);
				}

				Type = (PacketTypes)ReadInt(headerStream);
				int packetSize = ReadInt(headerStream);

				// Read the packet
				byte[] packet;
				if(packetSize > 0) { // Has a body
					packet = new byte[packetSize];
					bytesRead = 0;
					bytesReadThisPass = 0;

					do {
						try {
							bytesReadThisPass = clientStream.Read(packet, 0, packet.Length);
						} catch(Exception e) {
							throw e;
						}

						bytesRead += bytesReadThisPass;
					} while(bytesRead < packet.Length && bytesReadThisPass != 0);

					if(bytesReadThisPass == 0) throw new ConnectionInterruptedException();
				} else {
					// Packet with no body
					packet = new byte[0];
				}

				if(packetStream != null) {
					packetStream.Dispose();
					packetStream = null;
				}

				packetStream = new MemoryStream(packet);
			}
		}
		#endregion // Read Methods
		#endregion // Read/Write Methods
	}
}
